/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.velocityme.session;

import com.velocityme.entity.ChangeDelta;
import com.velocityme.entity.ChangeDeltaItem;
import com.velocityme.entity.Contactable;
import com.velocityme.entity.FileAttachment;
import com.velocityme.entity.Node;
import com.velocityme.entity.Person;
import com.velocityme.entity.Search;
import com.velocityme.entity.SearchLevel;
import com.velocityme.entity.SystemNode;
import com.velocityme.entity.Activity;
import com.velocityme.entity.Time;
import com.velocityme.enums.ChangeDeltaItemType;
import com.velocityme.enums.ChangeDeltaType;
import com.velocityme.enums.Permission;
import com.velocityme.utility.DurationRemaining;
import com.velocityme.utility.PermissionDeniedException;
import com.velocityme.utility.SearchEntry;
import com.velocityme.utility.TimeEntry;
import com.velocityme.utility.TimeSummaryTable;
import java.io.File;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ejb.EJB;
import javax.ejb.EJBException;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TemporalType;

/**
 *
 * @author rcrida
 */
@Stateless
public class NodeBean implements NodeLocal {
    @EJB
    private ActivityLocal activityBean;
    @EJB
    private ContactableLocal contactableBean;

    @PersistenceContext
    private EntityManager em;

    @EJB
    private SearchLocal m_searchLocal;

    public Node createNode(KeyLocal key, Node node) {
        key.assertHasPermission(node.getParentNode(), Permission.NODE_CREATE);
        ChangeDelta changeDelta = node.addChangeDelta(ChangeDeltaType.CREATE, key.getUser());
        logCreate(node, changeDelta);
        em.persist(node);
        em.persist(changeDelta);
        return node;
    }

    public void editNode(KeyLocal key, Node node) {
        key.assertHasPermission(node, Permission.NODE_EDIT);
        Node current = em.find(Node.class, node.getId());
        ChangeDelta changeDelta = node.addChangeDelta(ChangeDeltaType.EDIT, key.getUser());
        logEdit(node, current, changeDelta);
        em.persist(changeDelta);
        em.merge(node);
    }

    public void logCreate(final Node node, ChangeDelta changeDelta) {
        changeDelta.addItem(ChangeDeltaItemType.NAME, node.getName());
        changeDelta.addItem(ChangeDeltaItemType.DESCRIPTION, node.getDescription());
        changeDelta.addItem(ChangeDeltaItemType.PARENT_NODE, node.getParentNode().getPathName());
    }

    public void logEdit(final Node node, final Node current, ChangeDelta changeDelta) {
        if (!node.getName().equals(current.getName())) {
            changeDelta.addItem(ChangeDeltaItemType.NAME, node.getName());
        }
        if (!node.getDescription().equals(current.getDescription())) {
            changeDelta.addItem(ChangeDeltaItemType.DESCRIPTION, node.getDescription());
        }
    }

    public Node findNode(KeyLocal key, Long nodeId) {
        Node node = em.find(Node.class, nodeId);
        key.assertHasPermission(node, Permission.NODE_VIEW);
        em.refresh(node);
        return node;
    }

    public Map<String,Object> searchTimeLogValue(KeyLocal key, Node node, Collection<Contactable> contactables, boolean recursive, Set<SummaryTables> summaries, Date start, Date stop) {
        return searchTimeLogFilteredValue(key, node, new ArrayList<Node>(), contactables, recursive, summaries, start, stop);
    }

    public Map<String,Object> searchTimeLogFilteredValue(KeyLocal key, Node node, Collection<Node> excludedNodes, Collection<Contactable> contactables, boolean recursive, Set<SummaryTables> summaries, Date start, Date stop) {
        try {
            // set up the heading lists
            Collection<Set<Person>> personSets = new ArrayList<Set<Person>>();
            Collection<String> names = new ArrayList<String>();
            Set<Person> allPersons = new HashSet<Person>();
            for (Contactable contactable: contactables) {
                names.add(contactable.getName());
                Set<Person> personSet = contactableBean.getNestedPersonSet(key, contactable);
                personSets.add(personSet);
                allPersons.addAll(personSet);
            }
            // only add the total if there is more than one column of results
            if (names.size() > 1) {
                names.add("Total");
                personSets.add(allPersons);
            }
            String nodePathName = node.getPathName();
            Pattern pattern = Pattern.compile(nodePathName);
            DateFormat monthFormatter = new SimpleDateFormat("yyyy-MM");
            DateFormat weekFormatter = new SimpleDateFormat("yyyy-'W'ww");
            DateFormat dayFormatter = new SimpleDateFormat("yyyy-MM-dd EE");
            Search search = m_searchLocal.createSearchSpaceFiltered(key, node, excludedNodes, recursive);
            // create a search containing all the person nodes
            Collection<Node> personNodes = new ArrayList<Node>();
            for (Person person: allPersons) {
                personNodes.add(person);
            }
            Map<String,Object> results = new HashMap<String,Object>();
            List<TimeEntry> timeEntries = new ArrayList<TimeEntry>();
            Search searchPerson = new Search();
            SearchLevel searchLevelPerson = new SearchLevel(searchPerson, 0, personNodes);
            Collection<Time> times = (List<Time>)(em.createNamedQuery("findTimeForPersons")
                    .setParameter("search", search)
                    .setParameter("personSearch", searchPerson)
                    .setParameter("start", start)
                    .setParameter("stop", stop).getResultList());
            m_searchLocal.removeSearchSpace(key, search);
            m_searchLocal.removeSearchSpace(key, searchPerson);
            DurationAccummulator personAccummulator = new DurationAccummulator();
            DurationAccummulator taskAccummulator = new DurationAccummulator();
            DurationAccummulator taskTypeAccummulator = new DurationAccummulator();
            DurationAccummulator stateAccummulator = new DurationAccummulator();
            DurationAccummulator monthAccummulator = new DurationAccummulator();
            DurationAccummulator weekAccummulator = new DurationAccummulator();
            DurationAccummulator dayAccummulator = new DurationAccummulator();
            for (Time time: times) {
                long duration = time.getDuration();
                Person person = time.getPerson();
                Matcher matcher = pattern.matcher(time.getActivity().getPathName());
                String truncPathName = matcher.replaceFirst(".");
                // update the individual sums as required
                personAccummulator.add("Total", person, duration);
                if (summaries.contains(SummaryTables.TASK))
                    taskAccummulator.add(truncPathName, person, duration);
                if (summaries.contains(SummaryTables.TYPE))
                    taskTypeAccummulator.add(time.getActivity().getTaskStateMachine().toString(), person, duration);
                if (summaries.contains(SummaryTables.STATE))
                    stateAccummulator.add(time.getState().toString(), person, duration);
                if (summaries.contains(SummaryTables.MONTH))
                    monthAccummulator.add(monthFormatter.format(time.getStartTime()), person, duration);
                if (summaries.contains(SummaryTables.WEEK))
                    weekAccummulator.add(weekFormatter.format(time.getStartTime()), person, duration);
                if (summaries.contains(SummaryTables.DAY))
                    dayAccummulator.add(dayFormatter.format(time.getStartTime()), person, duration);
                // create the entries as required
                if (summaries.contains(SummaryTables.ENTRIES)) {
                    timeEntries.add(new TimeEntry(time.getPerson().getName(),
                                                  truncPathName,
                                                  time.getState().getName(),
                                                  time.getStartTime(),
                                                  time.getDuration(),
                                                  time.getDescription()));
                }
            }
            if (summaries.contains(SummaryTables.ENTRIES)) {
                // sort the time entries by start time
                Collections.sort(timeEntries, new Comparator<TimeEntry>() {
                    public int compare(TimeEntry o1, TimeEntry o2) {
                        return (o1.getDate().compareTo(o2.getDate()));
                    }
                });
                results.put("Entries", timeEntries);
            }
            results.put("Names", names);
            Collection<TimeSummaryTable> summaryTable = new ArrayList<TimeSummaryTable>();
            summaryTable.add(personAccummulator.getSummaryTable("Person", personSets));
            if (summaries.contains(SummaryTables.TASK))
                summaryTable.add(taskAccummulator.getSummaryTable("Task", personSets));
            if (summaries.contains(SummaryTables.TYPE))
                summaryTable.add(taskTypeAccummulator.getSummaryTable("Type", personSets));
            if (summaries.contains(SummaryTables.STATE))
                summaryTable.add(stateAccummulator.getSummaryTable("State", personSets));
            if (summaries.contains(SummaryTables.MONTH))
                summaryTable.add(monthAccummulator.getSummaryTable("Month", personSets));
            if (summaries.contains(SummaryTables.WEEK))
                summaryTable.add(weekAccummulator.getSummaryTable("Week", personSets));
            if (summaries.contains(SummaryTables.DAY))
                summaryTable.add(dayAccummulator.getSummaryTable("Day", personSets));
            results.put("Summaries", summaryTable);
            return results;
        } catch(Exception e) {
            throw new EJBException(e);
        }
    }

    /** Helper class which is used to sum durations for combinations of an id
     * and a personPK. The id could be for a Task or a State or a TaskStateMachine.
     * It is assumed that the id is comparable so that the results will come out
     * in some sort of order.
     */
    private class DurationAccummulator {
        private Map<Object,Map<Person,Long>> m_mapValuePerson = new TreeMap<Object,Map<Person,Long>>();
        public DurationAccummulator() {
        }
        public void add(Object id, Person person, long duration) {
            if (!m_mapValuePerson.containsKey(id))
                m_mapValuePerson.put(id, new HashMap<Person,Long>());
            Map<Person,Long> mapPersonSum = m_mapValuePerson.get(id);
            if (!mapPersonSum.containsKey(person))
                mapPersonSum.put(person, duration);
            else {
                long sum = mapPersonSum.get(person);
                sum += duration;
                mapPersonSum.put(person, sum);
            }
        }
//        private Iterator getIdIterator() {
//            return m_mapValuePerson.keySet().iterator();
//        }
        private List<Long> getSumsForId(Object id, Collection<Set<Person>> personSets) {
            List<Long> sums = new ArrayList<Long>();
            Map<Person,Long> mapPersonSum = m_mapValuePerson.get(id);
            for (Set<Person> personSet: personSets) {
                long sum = 0;
                for (Person person: personSet) {
                    Long personSum = (Long)mapPersonSum.get(person);
                    if (personSum != null)
                        sum += personSum.longValue();
                }
                sums.add(new Long(sum));
            }
            return sums;
        }
//        public void addSummaryTable(Document p_document, Element p_parentElement, String p_name, List p_setNames, List p_personSets) {
//            Element stElement = p_document.createElement("SummaryTable");
//            p_parentElement.appendChild(stElement);
//            stElement.setAttribute("name", p_name);
//            // put in the list of column names
//            Iterator nt = p_setNames.iterator();
//            while(nt.hasNext()) {
//                Element nameElement = p_document.createElement("Name");
//                stElement.appendChild(nameElement);
//                nameElement.setAttribute("value", nt.next().toString());
//            }
//            // now add a row for each task
//            Iterator ts = getIdIterator();
//            while(ts.hasNext()) {
//                Element valueElement = p_document.createElement("Value");
//                stElement.appendChild(valueElement);
//                Object id = ts.next();
//                valueElement.setAttribute("name", id.toString());
//                List sums = getSumsForId(id, p_personSets);
//                Iterator pt = sums.iterator();
//                while(pt.hasNext()) {
//                    Element sumElement = p_document.createElement("Sum");
//                    valueElement.appendChild(sumElement);
//                    sumElement.setAttribute("value", DurationFormat.formatDuration(((Long)pt.next()).longValue()));
//                }
//            }
//        }
        public TimeSummaryTable getSummaryTable(String name, Collection<Set<Person>> personSets) {
            TimeSummaryTable results = new TimeSummaryTable(name);
            // now add a row for each task
            for (Object id: m_mapValuePerson.keySet()) {
                String idName = id.toString();
                List<Long> sums = getSumsForId(id, personSets);
                Long[] longArray = sums.toArray(new Long[sums.size()]);
                results.addRow(idName, longArray);
            }
            return results;
        }
    }

    public SearchEntry getHistory(KeyLocal key, Node node) {
        if (m_searchLocal.getAssignedPermissions(key, node).contains(Permission.NODE_VIEW)) {
            SearchEntry searchEntry = new SearchEntry(node.getId(), 0);
            List<ChangeDelta> created = (List<ChangeDelta>)em.createNamedQuery("findChangeDeltaByType").
                    setParameter("node", node).
                    setParameter("changeDeltaType", ChangeDeltaType.CREATE).
                    getResultList();
            if (created.size() > 0) {
                ChangeDelta changeDelta = created.get(0);
                SearchEntry.Transaction transaction = searchEntry.addTransaction(changeDelta.getChangeDeltaType().toString(), changeDelta);
                transaction.addRow("Description", node.getDescription());
            }
            // get a collection of all the change deltas
            Set<ChangeDeltaItemType> acceptableItems = new HashSet<ChangeDeltaItemType>();
            acceptableItems.add(ChangeDeltaItemType.DESCRIPTION);
            acceptableItems.add(ChangeDeltaItemType.STATE);
            acceptableItems.add(ChangeDeltaItemType.STATUS);
            acceptableItems.add(ChangeDeltaItemType.MESSAGE);
            acceptableItems.add(ChangeDeltaItemType.RESPONSIBILITY);
            for (ChangeDelta changeDelta: (List<ChangeDelta>) em.createNamedQuery("findChangeDeltaByNode").setParameter("node", node).getResultList()) {
                SearchEntry.Transaction transaction = null;
                if (changeDelta.getChangeDeltaType().equals(ChangeDeltaType.CHANGE_STATE))
                    transaction = searchEntry.addTransaction("Change", changeDelta);
                if (changeDelta.getChangeDeltaType().equals(ChangeDeltaType.POST_COMMENT))
                    transaction = searchEntry.addTransaction("Comment", changeDelta);
                if (changeDelta.getChangeDeltaType().equals(ChangeDeltaType.REASSIGN_RESPONSIBILITY))
                    transaction = searchEntry.addTransaction("Reassign", changeDelta);
                if (transaction != null) {
                    for (ChangeDeltaItem changeDeltaItem: changeDelta.getChangeDeltaItems()) {
                        // only put acceptable change log items into the history
                        if (acceptableItems.contains(changeDeltaItem.getChangeDeltaItemType()))
                            transaction.addRow(changeDeltaItem.getChangeDeltaItemType().toString(),
                                               changeDeltaItem.getDescription());
                    }
                }
            }
            return searchEntry;
        }
        else
            throw new PermissionDeniedException();
    }

    public void postComment(KeyLocal key, Long nodeId, String message, File file, String contentType, InputStream stream) {
        Node node = findNode(key, nodeId);
        key.assertHasPermission(node, Permission.NODE_POST_COMMENT);
        ChangeDelta changeDelta = node.addChangeDelta(ChangeDeltaType.POST_COMMENT, key.getUser());
        changeDelta.addItem(ChangeDeltaItemType.MESSAGE, message);
        FileAttachment fileAttachment = new FileAttachment(changeDelta, file, contentType, stream);
        em.persist(changeDelta);
        if (node instanceof Activity) {
            Activity task = (Activity)node;
//            NotificationMechanismSessionUtil.getLocalHome().create().sendNotificationOfInterest(key, task, new HashSet(task.getInterestedContactables()));
        }
    }

    public Node deleteNode(KeyLocal key, Long nodeId) {
        Node node = em.find(Node.class, nodeId);
        key.assertHasPermission(node, Permission.NODE_DELETE);
        // can't delete system nodes
        if (node instanceof SystemNode) {
            throw new PermissionDeniedException();
        }
        Node parentNode = node.getParentNode();
        ChangeDelta changeDelta = parentNode.addChangeDelta(ChangeDeltaType.DELETE_NODE, key.getUser());
        changeDelta.addItem(ChangeDeltaItemType.DELETE_CHILD, node.getName());
        node.setDeleted(true);
        em.merge(node);
        em.persist(changeDelta);
        return parentNode;
    }

    public Map<String,DurationRemaining> searchDurationSum(KeyLocal key, Node node, boolean includeDuration, boolean recursive, Date start, Date stop, boolean singleBin) {
        Search search = m_searchLocal.createSearchSpace(key, node, recursive);
        Collection<Time> times = em.createNamedQuery("findTimeByDate").
                setParameter("search", search).
                setParameter("start", start, TemporalType.DATE).
                setParameter("stop", stop, TemporalType.DATE).
                getResultList();
        Collection<Activity> activities = em.createNamedQuery("findActivities").setParameter("search", search).getResultList();
        m_searchLocal.removeSearchSpace(key, search);
        return calculateDurationRemaining(activities, times, includeDuration, true, start, stop, singleBin);
    }

    private java.util.Map<String,DurationRemaining> calculateDurationRemaining(Collection<Activity> activities,
                                                     Collection<Time> times,
                                                     boolean includeDuration,
                                                     boolean includeRemaining,
                                                     Date start,
                                                     Date stop,
                                                     boolean singleBin) {
        class TDDR {
            public long taskId;
            public Date date;
            public long duration_min;
            public int remaining_min;
            public TDDR(long taskId, Date date, long duration_min, int remaining_min) {
                this.taskId = taskId;
                this.date = date;
                this.duration_min = duration_min;
                this.remaining_min = remaining_min;
            }
        }
        SimpleDurationAccummulator accummulator = new SimpleDurationAccummulator(start, stop, singleBin);
        List<TDDR> entries = new ArrayList<TDDR>();
        if (includeRemaining) {
            for (Activity activity: activities) {
                Collection<ChangeDelta> changeDeltas = em.createNamedQuery("findChangeDeltaByType").
                        setParameter("node", activity).
                        setParameter("changeDeltaType", ChangeDeltaType.CREATE).
                        getResultList();
                Date date = null;
                for (ChangeDelta cdl: changeDeltas) {
                    date = cdl.getCreationTime();
                }
                entries.add(new TDDR(activity.getId(), date, 0L, activityBean.getRemainingDuration_min(activity, start)));
            }
        }
        for (Time time: times) {
            long duration = includeDuration ? time.getDuration() : 0;
            long taskId = time.getActivity().getId();
            int remaining_min = includeRemaining ? time.getRemainingDuration_min() : 0;
            entries.add(new TDDR(taskId, time.getStartTime(), duration, remaining_min));
        }
        for (TDDR tddr: entries) {
            if (tddr.date.before(stop)) // exclude times after stop
                if (tddr.date.before(start))
                    accummulator.add(start, tddr.duration_min, tddr.taskId, tddr.remaining_min); // put all before start into first bin
                else
                    accummulator.add(tddr.date, tddr.duration_min, tddr.taskId, tddr.remaining_min);
        }
        return accummulator.getSums();
    }

    /** Helper class which is used to sum times. It chooses a suitable formatter
     * based on the date range.
     */
    private class SimpleDurationAccummulator {

        private Map<String,Long> m_mapSum = new TreeMap<String,Long>();
        private Map m_mapRemaining = new TreeMap();
        private DateFormat m_formatter;

        private class DateRemaining {

            public Date date;
            public int remaining_min;

            public DateRemaining(Date date, int remaining_min) {
                this.date = date;
                this.remaining_min = remaining_min;
            }
        }

        public SimpleDurationAccummulator(Date p_start, Date p_stop) {
            this(p_start, p_stop, false);
        }

        public SimpleDurationAccummulator(Date p_start, Date p_stop, boolean single) {
            Date dateDiff = new Date(p_stop.getTime() - p_start.getTime());
            int field = Calendar.DAY_OF_YEAR;
            m_formatter = new SimpleDateFormat("yyyy-MM-dd EE");
            if (dateDiff.getMonth() > 1) {
                m_formatter = new SimpleDateFormat("yyyy-'W'ww");
                field = Calendar.WEEK_OF_YEAR;
            }
            if (dateDiff.getMonth() > 6) {
                m_formatter = new SimpleDateFormat("yyyy-MM");
                field = Calendar.MONTH;
            }
            if (dateDiff.getYear() - 70 > 3) {
                m_formatter = new SimpleDateFormat("yyyy");
                field = Calendar.YEAR;
            }
            if (single) {
                String format = "-";
                m_formatter = new SimpleDateFormat(format);
                m_mapSum.put(format, new Long(0));
                m_mapRemaining.put(format, new HashMap());
            }
            Calendar start = Calendar.getInstance();
            start.setTime(p_start);
            start.set(Calendar.SECOND, 0);
            start.set(Calendar.MINUTE, 0);
            start.set(Calendar.HOUR_OF_DAY, 0);
            Calendar stop = Calendar.getInstance();
            stop.setTime(p_stop);
            for (; start.before(stop); start.add(field, 1)) {
                String id = m_formatter.format(start.getTime());
                m_mapSum.put(id, new Long(0));
                m_mapRemaining.put(id, new HashMap());
            }
        }

        public void add(Date date, long p_duration, long taskId, int remaining_min) {
            String id = m_formatter.format(date);
            //FIXME: there shouldn't be missing ids so why is this necessary!?
            if (!m_mapSum.containsKey(id)) {
                m_mapSum.put(id, 0L);
                m_mapRemaining.put(id, new HashMap());
            }
            long sum = (Long) m_mapSum.get(id);
            sum += p_duration;
            m_mapSum.put(id, new Long(sum));
            // now do remaining
            Map remaining = (Map) m_mapRemaining.get(id);
            if (!remaining.containsKey(taskId)) {
                remaining.put(taskId, new DateRemaining(date, remaining_min));
            } else {
                DateRemaining dr = (DateRemaining) remaining.get(taskId);
                if (dr.date.before(date)) {
                    dr.remaining_min = remaining_min;
                }
            }
        }

        public Map<String,DurationRemaining> getSums() {
            Map<String,DurationRemaining> integratedMap = new TreeMap<String,DurationRemaining>();
            Map integratedRemaining = new HashMap();
            int durationSum_min = 0;
            for (String id: m_mapSum.keySet()) {
                long sum = m_mapSum.get(id);
                durationSum_min += sum / 1000L / 60L;
                // now do remaining
                Map remaining = (Map) m_mapRemaining.get(id);
                integratedRemaining.putAll(remaining);
                Iterator j = integratedRemaining.values().iterator();
                int remainingSum_min = 0;
                while (j.hasNext()) {
                    DateRemaining dr = (DateRemaining) j.next();
                    remainingSum_min += dr.remaining_min;
                }
                integratedMap.put(id, new DurationRemaining(durationSum_min, remainingSum_min));
            }
            return integratedMap;
        }
    }

    @Override
    public List<ChangeDelta> getChangeDeltas(KeyLocal key, Node node) {
        key.assertHasPermission(node, Permission.NODE_VIEW);
        return em.createNamedQuery("findChangeDeltaByNode").setParameter("node", node).getResultList();
    }

    public List<Node> getLinkedNodes(KeyLocal key, Node node) {
        key.assertHasPermission(node, Permission.NODE_VIEW);
        return em.createNamedQuery("findLinkedNodes").setParameter("node", node).getResultList();
    }

    public Map<String,DurationRemaining> searchUnplannedSum(KeyLocal key, Collection<Person> persons, boolean recursive, Date start, Date stop, boolean singleBin) {
        // create a search containing all the person nodes
        Collection<Node> personNodesLocal = new ArrayList<Node>();
        for(Person person: persons) {
            personNodesLocal.add(person);
        }
        Search search = new Search();
        SearchLevel searchLevelPerson = new SearchLevel(search, 0, personNodesLocal);
        em.persist(search);
        Collection<Time> timesLocal = em.createNamedQuery("findAllTimeForPersons").
                setParameter("personSearch", search).
                setParameter("start", start).
                setParameter("stop", stop).
                getResultList();
        em.remove(search);
        return calculateDurationRemaining(new ArrayList(), timesLocal, true, false, start, stop, singleBin);
    }

}
